package org.zhuzx.util;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * web相关工具类
 * @author	zhuzx	2016年11月4日
 */
public class WebUtil {
	
	private static final String[] proxyForwardHeaderKeyAboutIp = {
			"x-forwarded-for",//HTTP代理或者负载均衡服务器
			"Proxy-Client-IP",//apache HTTP服务器
			"WL-Proxy-Client-IP",//weblogic插件
			"X-Real-IP",//nginx代理
			"HTTP_CLIENT_IP"//其它
	};
	
	/**
	 * 从HttpServletRequest中获取普通文本参数
	 */
	@Deprecated//仅本机测试备用
	public static Map<String,String> getParams(HttpServletRequest request) {
		Enumeration<String> paramNames = request.getParameterNames();
		Map<String,String> params = new HashMap<String,String>();
		boolean isGet = request.getMethod().equals("GET");
		String urlParams = request.getQueryString();
		Charset decoding = GeneralUtil.isBlank(urlParams) ? null : RttiUtil.getDecodeCharsetOfQueryString(request);
		boolean needTranscoding = (decoding != null) && !decoding.name().equals("UTF-8");
		while (paramNames.hasMoreElements()) {
			String key = paramNames.nextElement();
			String value = request.getParameter(key);
			//对于GET请求所有参数都需要转码
			//@Deprecated//GET参数可直接解析queryString，性能更佳，但request内部的解析流程较复杂，不宜贸然替代。
			if (isGet && !GeneralUtil.isBlank(value) && needTranscoding) {
				value = new String(value.getBytes(decoding), StandardCharsets.UTF_8);
			}
			params.put(key, value);
		}
		//对于POST请求，也需要对直接跟在url后面的参数进行转码。
		if (!isGet && !GeneralUtil.isBlank(urlParams) && needTranscoding) {
			try {
				urlParams = URLDecoder.decode(urlParams, "UTF-8");
			} catch (UnsupportedEncodingException e) {}
			urlParams = new String(urlParams.getBytes(decoding), StandardCharsets.UTF_8);
			for (String pair : MicroUtil.splitByChar(urlParams, '&')) {
				String[] temp = MicroUtil.splitByChar(pair, '=');
				params.put(temp[0], temp[1]);
			}
		}
		return params;
	}

	/**
	 * 从HttpServletRequest中获取响应头信息
	 */
	public static Map<String,String> getHeaders(HttpServletRequest request) {
		Enumeration<String> headerNames = request.getHeaderNames();
		Map<String,String> headers = new HashMap<String,String>();
		while (headerNames.hasMoreElements()) {
			String key = headerNames.nextElement();
			String value = request.getHeader(key);
			headers.put(key, value);
		}
		return headers;
	}

	/**
	 * 将Map中的参数以缀加形式转移到url中
	 * @return 新的url，携带有全部参数。
	 */
	public static String transferParamsIntoUrl(String basicUrl, Map<String,String> params) {
		if (GeneralUtil.isEmpty(params)) {
			return basicUrl;
		}
		StringBuilder url = new StringBuilder(basicUrl);
		if (url.indexOf("?") == -1) {
			url.append('?');
		} else {
			url.append('&');
		}
		for (Map.Entry<String, String> entry : params.entrySet()) {
			url.append(entry.getKey()).append('=').append(entry.getValue()).append('&');
		}
		url.deleteCharAt(url.length() - 1);
		return url.toString();
	}
	
	/**
	 * 将url中的参数转移到Map中
	 * @return 新的url，已去除其中携带的参数。
	 */
	public static String transferParamsIntoMap(String url, Map<String,String> params) {
		int index = url.indexOf('?');
		if (index > -1) {
			String urlParams = url.substring(index + 1);
			for (String pair : MicroUtil.splitByChar(urlParams, '&')) {
				String[] temp = MicroUtil.splitByChar(pair, '=');
				if (temp.length < 2) {
					params.put(temp[0], "");
				} else {
					params.put(temp[0], temp[1]);
				}
			}
			url = url.substring(0, index);
		}
		return url;
	}
	
	/**
	 * 创建{@code Map<String,Object>}的{@code Map<String,String>}副本
	 */
	public static Map<String,String> createStringTypeCopy(Map<String,Object> map) {
		Map<String,String> newMap = (map instanceof LinkedHashMap) ?
				new LinkedHashMap<String,String>(map.size()) : new HashMap<String,String>(map.size());
		for (Map.Entry<String,Object> entry : map.entrySet()) {
			newMap.put(entry.getKey(), GeneralUtil.toString(entry.getValue()));
		}
		return newMap;
	}
	
	/**
	 * 创建{@code List<Map<String,Object>>}的{@code List<Map<String,String>>}副本
	 */
	public static List<Map<String,String>> createStringTypeCopy(List<Map<String,Object>> list) {
		List<Map<String,String>> newList = new ArrayList<>(list.size());
		for (Map<String,Object> map : list) {
			newList.add(createStringTypeCopy(map));
		}
		return newList;
	}
	
	/**
	 * 根据二维数组生成json数组，二维数组的第一行定义键名，其余每行对应一个json对象的各个属性值。
	 * @return 以中括号包纳的json字符串
	 */
	public static String createJsonFromMultidimeArray(String[][] mdArray) {
		if (mdArray == null) return null;
		if (mdArray.length < 2) return "[]";
		StringBuilder sb = new StringBuilder();
		sb.append("[");
		String[] keys = mdArray[0];
		for (int i=1; i<mdArray.length; i++) {
			sb.append("{");
			String[] values = mdArray[i];
			for (int j=0; j<keys.length; j++) {
				sb.append("\"" + keys[j] + "\":\"" + values[j] + "\",");
			}
			sb.deleteCharAt(sb.length()-1);
			sb.append("},");
		}
		sb.deleteCharAt(sb.length()-1);
		sb.append("]");
		return sb.toString();
	}
	
	/**
	 * 根据ListMap生成json数组
	 * @return 以中括号包纳的json字符串
	 */
	public static String createJsonFromListMap(List<Map<String,String>> list) {
		if (list == null) return null;
		if (list.size() == 0) return "[]";
		StringBuilder json = new StringBuilder(list.size() * (list.get(0).size() + 2) + 2);
		json.append("[");
		for (Map<String,String> rec : list) {
			json.append("{");
			for (Map.Entry<String,String> entry : rec.entrySet()) {
				String value = (entry.getValue() == null) ? null : ("\"" + entry.getValue() + "\"");
				json.append("\"" + entry.getKey() + "\":" + value + ",");
			}
			if (rec.size() > 0) {
				json.deleteCharAt(json.length() - 1);
			}
			json.append("},");
		}
		json.deleteCharAt(json.length() - 1);
		json.append("]");
		return json.toString();
	}
	
	/**
	 * 根据状态码和描述信息生成json字符串，格式为{"code":${codeValue}, "msg":"${msgValue}"}。
	 */
	public static String createJsonFromPair(int code, String msg) {
		return "{\"code\":" + code + ",\"msg\":\"" + msg + "\"}";
	}
	
	/**
	 * 解析json串生成一个纯文本Map，不支持解析json数组（可使用createListMapFromJsonArray方法）。<br>
	 * 仅解析第一级结构，所有的值都会被当成纯文本处理。<br>
	 * 若想解析内嵌的对象或者数组，可以从返回的Map中取得纯文本后再调用解析方法。<br>
	 * @return LinkedHashMap对象，其键值对按json串中声明的顺序排列。
	 */
	public static Map<String,String> createMapFromJson(String json) {
		json = json.trim();
		boolean haveStartBorder = json.charAt(0) == '{';
		boolean haveEndBorder = json.charAt(json.length()-1) == '}';
		if (haveStartBorder && haveEndBorder) {
			json = json.substring(1, json.length()-1).trim();
		} else {
			throw new IllegalArgumentException("传入的json头尾不是{}");
		}
		if (json.length() == 0) return new LinkedHashMap<String,String>();
		Map<String, String> stowedValueMap = new HashMap<String, String>();
		Pattern pat = Pattern.compile(":\\s*[\\[\\{]");
		while (true) {
			Matcher mat = pat.matcher(json);
			if (mat.find()) {
				String result = mat.group();
				String stowedValue;
				if (result.charAt(result.length()-1) == '[') {
					stowedValue = "[" + GeneralUtil.getNotBlankString(
							MicroUtil.substringByMatesOnRegex(json, "[^\\\\]\\[", "[^\\\\]]"), "") + "]";
				} else {
					stowedValue = "{" + GeneralUtil.getNotBlankString(
							MicroUtil.substringByMatesOnRegex(json, "[^\\\\]\\{", "[^\\\\]}"), "") + "}";
				}
				int valueStart = mat.end() - 1;
				if (stowedValue.length() > 2) {
					stowedValue = stowedValue.substring(0, stowedValue.length()-1)
							+ json.charAt(valueStart+stowedValue.length()-1)
							+ stowedValue.charAt(stowedValue.length()-1);
				}
				int keyEnd = json.lastIndexOf('"', valueStart);
				String key = json.substring(json.lastIndexOf('"', keyEnd-1) + 1, keyEnd);
				stowedValueMap.put(key, stowedValue);
				json = json.substring(0, valueStart) + ":" + json.substring(valueStart + stowedValue.length());
			} else {
				break;
			}
		}
		//去掉第一个引号，分割后第一个键值对和其它键值对格式统一。
		json = json.substring(1);
		String[] pairs = json.split(",\\s*\"");
		Map<String,String> map = new LinkedHashMap<String,String>();
		for (String pair : pairs) {
			pair = pair.trim();
			int index = pair.indexOf(':');
			String key = pair.substring(0, index).trim();
			key = key.substring(0, key.length()-1);
			String value = pair.substring(index+1).trim();
			if ((value.charAt(0) == '"') && (value.charAt(value.length()-1) == '"')) {
				value = value.substring(1, value.length()-1);
			} else if (value.equals("null")) {
				value = null;
			}
			map.put(key, value);
		}
		map.putAll(stowedValueMap);
		return map;
	}
	
	/**
	 * 解析json数组格式的字符串，生成一个纯文本Map列表，其中每个Map对应json数组中的一个对象。
	 */
	public static List<Map<String,String>> createListMapFromJsonArray(String json) {
		json = json.trim();
		boolean haveStartBorder = json.charAt(0) == '[';
		boolean haveEndBorder = json.charAt(json.length()-1) == ']';
		if (haveStartBorder && haveEndBorder) {
			json = json.substring(1, json.length()-1).trim();
		} else {
			throw new IllegalArgumentException("传入的json头尾不是[]");
		}
		if (json.length() == 0) return new ArrayList<Map<String, String>>();
		//去掉第一个元素的{和最后一个元素的}，分割后和其它元素格式统一。
		json = json.substring(1, json.length()-1);
		String[] elements = json.split("}\\s*,\\s*\\{");
		List<Map<String, String>> list = new ArrayList<Map<String, String>>(elements.length);
		for (String jsonEle : elements) {
			list.add(createMapFromJson("{" + jsonEle + "}"));
		}
		return list;
	}
	
	/**
	 * 将一个用分隔符连接的合并字符串重新分割，再根据comparator排序后重新合并。
	 * @param combinedString 原始字符串
	 * @param separator 原始字符串中的分隔符
	 * @param comparator 排序依据
	 * @return 新的字符串，仍旧使用原来的分隔符分隔。
	 */
	public static String getSortedString(String combinedString, String separator,
			Comparator<String> comparator) {
		String[] temp = combinedString.split(separator);
		Arrays.sort(temp, comparator);
		StringBuilder sb = new StringBuilder(combinedString.length());
		for (String item : temp) {
			sb.append(separator + item);
		}
		return sb.substring(separator.length());
	}
	
	/**
	 * 比较两个Map中不一致的值
	 * @return 返回的Map中包含了所有存在差异的键，对应的两个值放在一个数组中作为value。
	 * 			数组第一个元素是map1中的键值，第二个元素是map2中的键值。
	 */
	public static Map<String,String[]> compareDifferences(Map<String,Object> map1, Map<String,Object> map2) {
		Map<String,String[]> result = (map1 instanceof LinkedHashMap) ?
				new LinkedHashMap<String,String[]>() : new HashMap<String,String[]>();
		Map<String,String> copy2 = createStringTypeCopy(map2);
		//先以map1中的全部键为基准进行比较
		for (Map.Entry<String,String> entry : createStringTypeCopy(map1).entrySet()) {
			String value2 = copy2.remove(entry.getKey());
			if (!Objects.equals(entry.getValue(), value2)) {
				String[] values = {entry.getValue(), value2};
				result.put(entry.getKey(), values);
			}
		}
		//并入map2中多出来的键值
		for (Map.Entry<String,String> entry : copy2.entrySet()) {
			result.put(entry.getKey(), new String[] {null, entry.getValue()});
		}
		return result;
	}
	
	/**
	 * 创建差异详情描述文本
	 * @param differences 所有差异键值对
	 * @param onlyForUpdate 是否只描述两个值都不为空的键
	 * @param dictionary 指定需要描述的键，以及这个键的展示名。
	 * @return 每个在dictionary中存在的键都会生成如下格式的一行文本并缀加一个\n：键的展示名 : value1 >> value2
	 */
	public static String createDifferencesDesc(Map<String,String[]> differences, boolean onlyForUpdate,
			Map<String,String> dictionary) {
		StringBuilder sb = new StringBuilder();
		for (Map.Entry<String,String[]> entry : differences.entrySet()) {
			String[] values = entry.getValue();
			if (onlyForUpdate && GeneralUtil.isBlank(values)) {
				continue;
			}
			String displayKey = dictionary.get(entry.getKey());
			if (displayKey == null && !dictionary.containsKey(entry.getKey())) {//字典中没有的key就不管
				continue;
			}
			sb.append(displayKey).append(" : ")
					.append(values[0]).append(" >> ").append(values[1]).append("\n");
		}
		if (sb.length() > 0) {
			sb.deleteCharAt(sb.length() - 1);
		}
		return sb.toString();
	}
	
	/**
	 * 获取请求来源ip。<br>
	 * 根据Druid的方案做了细微调整：DruidWebUtils.getRemoteAddr。
	 */
	public static String getRemoteAddr(HttpServletRequest request) {
		Map<String,String> headers = getHeaders(request);
		for (String key : proxyForwardHeaderKeyAboutIp) {
			String ip = headers.get(key);
			if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
				continue;
			} else {
				return ip;
			}
		}
		return request.getRemoteAddr();
	}
	
	/**
	 * 将request的所有内容转发到remoteUrl，并将得到的响应输出到response。
	 */
	@SuppressWarnings("unchecked")
	public static void transpondHttpRequest(HttpServletRequest request, HttpServletResponse response, String remoteUrl) {
		Map<String, String> headers = getHeaders(request);
		if (remoteUrl == null) {
			//Header参数名不能含下划线，会被nginx之类的服务器自动屏蔽。自定义的参数名不能使用大写，因为可能会被自动转成小写。
			remoteUrl = headers.remove("remote-url");
			if (remoteUrl == null) {
				throw new IllegalArgumentException("未传入remote-url参数值");
			}
			remoteUrl = remoteUrl.trim();
			if (remoteUrl.startsWith("http%3A%2F%2F") || remoteUrl.startsWith("https%3A%2F%2F")) {
				try {//若未携带中文，则用iso8859编码的也能以utf8正确解码还原。
					remoteUrl = URLDecoder.decode(remoteUrl, "UTF-8");
				} catch (UnsupportedEncodingException e) {}
			}
		}
		try {
			Object[] result = NetUtil.executeRequestCrudely(remoteUrl,
					GeneralUtil.getBytes(request.getInputStream()), headers, request.getMethod());
			Map<String,String> resHeaders = (Map<String, String>) result[1];
			/*
			 * 远程服务器返回的两个内容传输相关的响应头，因为本地服务器会根据情况自动加其中一个，有可能和远程响应头造成冲突。
			 * 比如，远程服务器已返回Transfer-Encoding=chunked，但本地服务器可能会再加上一个响应头Content-Length=xxx，将导致报错：
			 * Parse Error: The response headers can't include "Content-Length" with chunked encoding
			 * 从理论层面分析，向远程服务器发起请求，得到的响应头是用于指示客户端（也就是本机）如何接收数据，请求结束后，这两个响应头已完成任务。
			 * 本机的response对象的响应头，用于指示本服务器跟前端（或者说客户端）如何传输数据，使用何种响应头完全取决于本服务器的策略，
			 * 而跟远程服务器没有任何关系，故此处不能用它给的响应头原样返回给我们的客户端。
			 */
			resHeaders.remove("Content-Length");
			resHeaders.remove("Transfer-Encoding");
			byte[] resBytes = (byte[]) result[0];
			/*
			 * 本服务器在返回数据时，完全有可能改变Content-Encoding的值，甚至可能会在这个已压缩的基础上再压缩一道，
			 * 所以必须将数据还原，并且不使用远程服务器的响应头，由本服务器自行添加，道理同上。
			 * 注：目前仅能处理Content-Encoding=gzip的响应头，其它情况直接放行，对数据和响应头都不加处理，原样返给客户端。
			 * a：关于NetUtil.isGzip方法中对于Content-Disposition的判断逻辑，并非规范的压缩指示方式，此处不予采用。
			 * b：人力有限，暂只处理gzip类型，待添加deflate等其它格式的处理逻辑。
			 */
			String enco = resHeaders.get("Content-Encoding");
			if (!GeneralUtil.isBlank(enco) && enco.trim().equalsIgnoreCase("gzip")) {
				resBytes = FileOperationUtil.gzipUnCompressToBytes(resBytes);
				resHeaders.remove("Content-Encoding");
			}
			//其它响应头，比如Content-Type、Set-Cookie等，原样返给客户端。
			for (Map.Entry<String,String> entry : resHeaders.entrySet()) {
				response.setHeader(entry.getKey(), entry.getValue());
			}
			OutputStream os = response.getOutputStream();
			os.write(resBytes);
			os.close();
		} catch (IOException e) {
			throw new RuntimeException("转发至远程服务器时发生错误：" + remoteUrl, e);
		}
	}

	/**
	 * 通过ServletResponse对象直接发送数据给客户端
	 */
	public static void sendDataByServlet(ServletResponse response, String msg) {
		String type = msg.trim().startsWith("{") ? "application/json" : "text/plain";
		response.setContentType(type + "; charset=UTF-8");
		try (PrintWriter writer = response.getWriter()) {
			writer.write(msg);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

}